Tento soubor je součástí sestavy elektronických studijních opor Příběhy dat: Výpočetní přístupy ke studiu kultury a společnosti.
Formální síťová analýza¶
autor: Vojtěch Kaše (kase@ff.zcu.cz)
Úvod a cíle kapitoly¶
V tomto notebooku si budeme prakticky osvojovat koncepty síťové analýzy. Z veřejně dostupných dat si vytvoříme několik síťových grafů, které budeme dále upravovat, analyzovat a vizualizovat.
Jedním z nejhodnotnějších typů historických dat jsou sbírky dopisů, které nám umožňují sledovat kdo, s kým a kdy udřžoval kontakty. Řada těchto dopisních sbírek byla v posledních dekádách digitalizována. Existují tak například digitalizované kolekce sbírkek dopisů středověkých žen (https://epistolae.ctl.columbia.edu/letters/) nebo rozsáhlá kolekce raně novověkých dopisů EMLO (=Early Modern Letters Online, http://emlo-portal.bodleian.ox.ac.uk). Některé tyto datasety umožňují přístup pouze pomocí prohlížeče, a tudíž se nehodí pro datově analytickou práci. Jiné jsou naopak vzorovými příklady datového kurátorství. Ty zde budeme používat.
Konkrétně využijeme dataset dopisů mezi britskými vědci konce 18. a celého 19. století Ɛpsilon (web), vyvíjený týmem z Cambridge University Digital Library.
Ɛpsilon opens up new research opportunities in the history of 19th century science by bringing correspondence data and transcriptions from multiple sources into a single cross-searchable digital platform. It currently holds details of over 50,000 letters and is growing.
Alespoň z pohledu datové analýzy je velkou devízou tohoto projektu fakt, že veškerá data jsou dostupná nejen pro potřeby prohledávání a pročítání na webu projektu, ale také ve velice úhledné a praktické formě dostupná na GitHubu (zde). Nachází se zde jak digitální edice každého jednotlivého dopisu podle standardu TEI-XML, tak i tabulky metadat ve formátu CSV. S těmi budeme níže pracovat my, když se je přímo z GitHubu načteme do našeho výpočetního prostředí.
Nejprve budeme pracovat s kolekcí dopisů Londínské Linneovské společnosti, která byla založena roku 1788 a existuje dodnes (wikipedia). Ač nese jméno významného švédského vědce Carla Linného (wikipedia), otce vědecké taxonomie, tato vědecká společnost vznikla v Anglii až po jeho smrti.
Tabulková data budeme zpracovávat pomocí knihovny pandas. K síťové analýze využijeme knihovnu networkX, jejíž dokumentaci doporučuji k projití si - zde).
Cvičení 1: Korespondence Linnevské společnosti¶
Extrakce a přehled dat¶
import numpy as np
import pandas as pd
import requests
import networkx as nx
import numpy as np
import matplotlib.pyplot as plt
import regex
from bs4 import BeautifulSoup
# navštívíme url adresu, kde jsou umístěny všechny csv soubory
resp_json = requests.get("https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/").json()
Nyní si vypíšeme obsah načtených dat a zorientujeme v příslušné struktuře:
resp_json
[{'name': 'ampere.csv',
'path': 'csv/ampere.csv',
'sha': 'eb3508f2630916d16fd58cd591c03539315b3e90',
'size': 218315,
'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/ampere.csv?ref=main',
'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/ampere.csv',
'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/eb3508f2630916d16fd58cd591c03539315b3e90',
'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/ampere.csv',
'type': 'file',
'_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/ampere.csv?ref=main',
'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/eb3508f2630916d16fd58cd591c03539315b3e90',
'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/ampere.csv'}},
{'name': 'darwin-correspondence.csv',
'path': 'csv/darwin-correspondence.csv',
'sha': 'b205b8185125a4771e5d9d59e47b1365b185f5b4',
'size': 2345357,
'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/darwin-correspondence.csv?ref=main',
'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/darwin-correspondence.csv',
'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/b205b8185125a4771e5d9d59e47b1365b185f5b4',
'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/darwin-correspondence.csv',
'type': 'file',
'_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/darwin-correspondence.csv?ref=main',
'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/b205b8185125a4771e5d9d59e47b1365b185f5b4',
'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/darwin-correspondence.csv'}},
{'name': 'darwin-family-letters.csv',
'path': 'csv/darwin-family-letters.csv',
'sha': '7b2e097b2ebcd3733aacfd96acd304ca9465c36a',
'size': 202122,
'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/darwin-family-letters.csv?ref=main',
'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/darwin-family-letters.csv',
'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/7b2e097b2ebcd3733aacfd96acd304ca9465c36a',
'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/darwin-family-letters.csv',
'type': 'file',
'_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/darwin-family-letters.csv?ref=main',
'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/7b2e097b2ebcd3733aacfd96acd304ca9465c36a',
'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/darwin-family-letters.csv'}},
{'name': 'faraday.csv',
'path': 'csv/faraday.csv',
'sha': 'e00aecced5d7e41ea96b2729fd3bb9fb57256b7e',
'size': 678078,
'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/faraday.csv?ref=main',
'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/faraday.csv',
'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/e00aecced5d7e41ea96b2729fd3bb9fb57256b7e',
'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/faraday.csv',
'type': 'file',
'_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/faraday.csv?ref=main',
'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/e00aecced5d7e41ea96b2729fd3bb9fb57256b7e',
'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/faraday.csv'}},
{'name': 'henslow.csv',
'path': 'csv/henslow.csv',
'sha': 'a90c956fb447fceadc9c630be0e21d9f811b3a39',
'size': 123802,
'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/henslow.csv?ref=main',
'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/henslow.csv',
'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/a90c956fb447fceadc9c630be0e21d9f811b3a39',
'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/henslow.csv',
'type': 'file',
'_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/henslow.csv?ref=main',
'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/a90c956fb447fceadc9c630be0e21d9f811b3a39',
'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/henslow.csv'}},
{'name': 'herschel.csv',
'path': 'csv/herschel.csv',
'sha': 'b277c0a6705954887671c2faf8bf56f4cc056b82',
'size': 2245836,
'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/herschel.csv?ref=main',
'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/herschel.csv',
'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/b277c0a6705954887671c2faf8bf56f4cc056b82',
'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/herschel.csv',
'type': 'file',
'_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/herschel.csv?ref=main',
'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/b277c0a6705954887671c2faf8bf56f4cc056b82',
'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/herschel.csv'}},
{'name': 'kemp.csv',
'path': 'csv/kemp.csv',
'sha': '59f90c9a8829fbb8828207c3f961d53192c5c3c2',
'size': 11685,
'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/kemp.csv?ref=main',
'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/kemp.csv',
'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/59f90c9a8829fbb8828207c3f961d53192c5c3c2',
'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/kemp.csv',
'type': 'file',
'_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/kemp.csv?ref=main',
'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/59f90c9a8829fbb8828207c3f961d53192c5c3c2',
'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/kemp.csv'}},
{'name': 'linnean-society.csv',
'path': 'csv/linnean-society.csv',
'sha': '7e0742fb8d07ec6960fd5b52aac58da4813dbd7a',
'size': 676746,
'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/linnean-society.csv?ref=main',
'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/linnean-society.csv',
'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/7e0742fb8d07ec6960fd5b52aac58da4813dbd7a',
'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/linnean-society.csv',
'type': 'file',
'_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/linnean-society.csv?ref=main',
'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/7e0742fb8d07ec6960fd5b52aac58da4813dbd7a',
'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/linnean-society.csv'}},
{'name': 'royal-society.csv',
'path': 'csv/royal-society.csv',
'sha': '06448ac2ac80b2948dd08f1def2d9a30bf418650',
'size': 493183,
'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/royal-society.csv?ref=main',
'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/royal-society.csv',
'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/06448ac2ac80b2948dd08f1def2d9a30bf418650',
'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/royal-society.csv',
'type': 'file',
'_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/royal-society.csv?ref=main',
'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/06448ac2ac80b2948dd08f1def2d9a30bf418650',
'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/royal-society.csv'}},
{'name': 'somerville.csv',
'path': 'csv/somerville.csv',
'sha': 'd298cfff8e5a817e51ef4742f8d2cacbb35d01a9',
'size': 98123,
'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/somerville.csv?ref=main',
'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/somerville.csv',
'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/d298cfff8e5a817e51ef4742f8d2cacbb35d01a9',
'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/somerville.csv',
'type': 'file',
'_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/somerville.csv?ref=main',
'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/d298cfff8e5a817e51ef4742f8d2cacbb35d01a9',
'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/somerville.csv'}},
{'name': 'tyndall.csv',
'path': 'csv/tyndall.csv',
'sha': '52618f0e2d35c109fdc948920d69219f7c0b3c47',
'size': 331433,
'url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/tyndall.csv?ref=main',
'html_url': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/tyndall.csv',
'git_url': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/52618f0e2d35c109fdc948920d69219f7c0b3c47',
'download_url': 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/tyndall.csv',
'type': 'file',
'_links': {'self': 'https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/tyndall.csv?ref=main',
'git': 'https://api.github.com/repos/cambridge-collection/epsilon-data/git/blobs/52618f0e2d35c109fdc948920d69219f7c0b3c47',
'html': 'https://github.com/cambridge-collection/epsilon-data/blob/main/csv/tyndall.csv'}}]
Vidíme, že ve struktuře je možné nalézt výpis jednotlivých csv souborů, které nás zajímají s odkazy na data ve formátu ke stažení ("download_url")
# vytvoříme si list URL adres všech csv souborů
download_urls = [item["download_url"] for item in resp_json]
download_urls
['https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/ampere.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/darwin-correspondence.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/darwin-family-letters.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/faraday.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/henslow.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/herschel.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/kemp.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/linnean-society.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/royal-society.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/somerville.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/tyndall.csv']
# a také list jmen všech těchto souborů
filenames = [item["name"] for item in resp_json]
filenames
['ampere.csv', 'darwin-correspondence.csv', 'darwin-family-letters.csv', 'faraday.csv', 'henslow.csv', 'herschel.csv', 'kemp.csv', 'linnean-society.csv', 'royal-society.csv', 'somerville.csv', 'tyndall.csv']
# načteme si data z jednoho konkrétního souboru
linnean = pd.read_csv("https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/linnean-society.csv")
linnean.head()
| id | sender_surname | sender_forename | recipient_surname | recipient_forename | sorting_date | date | sender_address | recipient_address | source | languages | extent | filename | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | LINNEAN1 | Abbot | Charles | Smith | Sir James Edward | 1807-11-02 | 2 Nov 1807 | Bedford, Bedfordshire | NaN | GB-110/JES/ADD/1, The Linnean Society of London | eng | NaN | LINNEAN1.xml |
| 1 | LINNEAN2 | Butt | John Martin | Smith | Sir James Edward | 1798-09-17 | 17 Sep 1798 | Witley, Worcestershire | NaN | GB-110/JES/ADD/10, The Linnean Society of London | eng | NaN | LINNEAN2.xml |
| 2 | LINNEAN3 | Strutt | Jacob George | Smith | Sir James Edward | 1826-05-31 | 31 May 1826 | London | NaN | GB-110/JES/ADD/100, The Linnean Society of London | eng | NaN | LINNEAN3.xml |
| 3 | LINNEAN4 | Swainson | William | Smith | Sir James Edward | 1815-04-22 | 22 Apr 1815 | Palermo, Sicily | London | GB-110/JES/ADD/101, The Linnean Society of London | eng | NaN | LINNEAN4.xml |
| 4 | LINNEAN5 | Teesdale | Robert | Smith | Sir James Edward | 1789-11-18 | 18 Nov 1789 | London | London | GB-110/JES/ADD/102, The Linnean Society of London | eng | NaN | LINNEAN5.xml |
Vidíme zde výpis prvních pěti řádek datové tabulky. Ale kolik vlastně tabulka čítá položek a kolik že je sloupců? To zjistíme z atributu shape (atributem je vlastnost datového objektu - jednou z vlastností datového objektu podle standardu pd.DataFrame je jeho tvar, tj. počet řádků a sloupců.
linnean.shape
(3538, 13)
Než se pustíme do síťových analýz, ještě si upravíme hodnoty v některých sloupcích tak, aby se nám s nimi dobře pracovalo. Sloupec "sorting_date" vyjadřuje dataci daného dopisu ve velice úhledném a srozumitelném formátu (yyyy-mm-dd). Jelikož jsme však naše data načetli z prostého csv souboru, Python neví nic o tom, že za touto řadou čísel a pomlček se jedná o dataci; k tomu jej musíme nainstruovat.
V buňce níže za tímto účelem vytváříme nový sloupec s výmluvným názvem "datetime". Hodnoty v tomto sloupci jsou výsledkem použití (aplikování) funkce to_datetime() z knihovny pandas (pd) na hodnoty ve sloupci "sorting_date". Tato funkce "přeloží" jednotlivá čísla na roky, měsíce a dny.
linnean["datetime"] = linnean["sorting_date"].apply(pd.to_datetime)
linnean.head(5)
| id | sender_surname | sender_forename | recipient_surname | recipient_forename | sorting_date | date | sender_address | recipient_address | source | languages | extent | filename | datetime | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | LINNEAN1 | Abbot | Charles | Smith | Sir James Edward | 1807-11-02 | 2 Nov 1807 | Bedford, Bedfordshire | NaN | GB-110/JES/ADD/1, The Linnean Society of London | eng | NaN | LINNEAN1.xml | 1807-11-02 |
| 1 | LINNEAN2 | Butt | John Martin | Smith | Sir James Edward | 1798-09-17 | 17 Sep 1798 | Witley, Worcestershire | NaN | GB-110/JES/ADD/10, The Linnean Society of London | eng | NaN | LINNEAN2.xml | 1798-09-17 |
| 2 | LINNEAN3 | Strutt | Jacob George | Smith | Sir James Edward | 1826-05-31 | 31 May 1826 | London | NaN | GB-110/JES/ADD/100, The Linnean Society of London | eng | NaN | LINNEAN3.xml | 1826-05-31 |
| 3 | LINNEAN4 | Swainson | William | Smith | Sir James Edward | 1815-04-22 | 22 Apr 1815 | Palermo, Sicily | London | GB-110/JES/ADD/101, The Linnean Society of London | eng | NaN | LINNEAN4.xml | 1815-04-22 |
| 4 | LINNEAN5 | Teesdale | Robert | Smith | Sir James Edward | 1789-11-18 | 18 Nov 1789 | London | London | GB-110/JES/ADD/102, The Linnean Society of London | eng | NaN | LINNEAN5.xml | 1789-11-18 |
Ač hodnoty ve sloupci "datetime" vypadají stejně jako hodnoty ve sloupci "sorting_date", chovají se odlišně. Umožňují nám přímo studovat časovou distribuci našich dat. Výhody tohoto formátu si všimneme, když na daný sloupec aplikujeme vizualizační metodu hist():
linnean["datetime"].hist()
<Axes: >
# snadno se můžeme např. podívat pouze na dopisy odeslané před začátkem 19. století
linnean["18thcent?"] = linnean["datetime"] < pd.to_datetime("1801-01-01")
# jen pro ověření se podívejme na prvních 5 řádek takto filtrovaných dat
linnean[linnean["18thcent?"]].head(5)
| id | sender_surname | sender_forename | recipient_surname | recipient_forename | sorting_date | date | sender_address | recipient_address | source | languages | extent | filename | datetime | 18thcent? | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | LINNEAN2 | Butt | John Martin | Smith | Sir James Edward | 1798-09-17 | 17 Sep 1798 | Witley, Worcestershire | NaN | GB-110/JES/ADD/10, The Linnean Society of London | eng | NaN | LINNEAN2.xml | 1798-09-17 | True |
| 4 | LINNEAN5 | Teesdale | Robert | Smith | Sir James Edward | 1789-11-18 | 18 Nov 1789 | London | London | GB-110/JES/ADD/102, The Linnean Society of London | eng | NaN | LINNEAN5.xml | 1789-11-18 | True |
| 5 | LINNEAN6 | Thunberg | Carl Peter | Smith | Sir James Edward | 1792-06-26 | 26 Jun 1792 | Uppsala, Sweden | NaN | GB-110/JES/ADD/103, The Linnean Society of London | fre | NaN | LINNEAN6.xml | 1792-06-26 | True |
| 7 | LINNEAN8 | Treschow | H | Smith | Sir James Edward | 1794-07-08 | 8 Jul 1794 | Copenhagen, Denmark | NaN | GB-110/JES/ADD/105, The Linnean Society of London | eng | NaN | LINNEAN8.xml | 1794-07-08 | True |
| 9 | LINNEAN10 | Camper | Petrus | Smith | Sir James Edward | 1788-06-22 | [22 Jun 1788] | The Hague, Netherlands | London | GB-110/JES/ADD/107, The Linnean Society of London | eng | NaN | LINNEAN10.xml | 1788-06-22 | True |
Vlastní jméno odesilatele a příjemce se nám rozpadá do vícero sloupců ("sender_surname", "sender_forename"). Vytvořme si nyní agregovanou podobu jména.
linnean["sender_agr"] = linnean.apply(lambda row: str(row["sender_surname"]).replace(" ", "_") + "_" + str(row["sender_forename"]).replace(" ", "_"), axis=1)
linnean["recipient_agr"] = linnean.apply(lambda row: str(row["recipient_surname"]).replace(" ", "_") + "_" + str(row["recipient_forename"]).replace(" ", "_"), axis=1)
Nyní se podíváme na osoby, který poslaly a přijaly největší množství dopisů:
linnean["sender_agr"].value_counts()
sender_agr
Smith_Sir_James_Edward 481
Goodenough_Samuel 222
Woodward_Thomas_Jenkinson 101
Roscoe_William 98
Johnes_Thomas 84
...
Erskine_David_Steuart 1
Upcher_Abbot 1
Walcott_William 1
Baker_William_Lloyd 1
Cullen_Charles_Sinclair 1
Name: count, Length: 457, dtype: int64
linnean["recipient_agr"].value_counts()
recipient_agr
Smith_Sir_James_Edward 2948
Macleay_Alexander 102
Smith_Pleasance 72
Roscoe_William 53
Unknown_nan 51
...
Sutton_Charles 1
Brandreth_Mrs 1
Bright_Richard 1
Walker_George 1
Reeve_Robert 1
Name: count, Length: 65, dtype: int64
V obou případech vidíme na prvním místě Sira Jamese Edwarda Smithe. Což, víme-li něco o Linneovské společnosti nebo podíváme-li se na wikipedii, není příliš překvapivé: jedná se o samotného zakladatele a dlouholetého předsedu této společnosti (viz wikipedia)).
V druhé tabulce vidíme na třetím místě také jeho manželku, Pleasance Smithovou, která byla taktéž významnou osobností dobového dění (taktéž viz wikipedie).
Tvorba síťových dat¶
Pro potřeby následujících si naše data výrazně přeskupíme a přetvoříme do podoby seznamu vážených vazeb.
linnean_edges = linnean.groupby(["sender_agr", "recipient_agr"]).size().reset_index()
linnean_edges.columns = ["sender_agr", "recipient_agr", "letters_n"]
linnean_edges.head()
| sender_agr | recipient_agr | letters_n | |
|---|---|---|---|
| 0 | Abbot_Charles | Smith_Sir_James_Edward | 18 |
| 1 | Acharius_Erik | Smith_Sir_James_Edward | 8 |
| 2 | Acrel_Johan_Gustaf | Smith_Sir_James_Edward | 7 |
| 3 | Afzelius_Adam | Smith_Sir_James_Edward | 14 |
| 4 | Aiton_William_Townsend | Smith_Sir_James_Edward | 1 |
Jednotkou pozorování (čili řádkou tabulky) nyní již není každý jednotlivý dopis, ale pár odesilatele a příjemce s informací, kolik odesilatel příjemci zaslal dopisů (viz sloupec "letters_n"). Tato data lze již v podstatě považovat za tabulku hran. Můžeme si je setřídit od těch s největší váhou (tj. s nejvyšším počtem dopisů poslaných daným směrem).
linnean_edges.sort_values("letters_n", ascending=False)
| sender_agr | recipient_agr | letters_n | |
|---|---|---|---|
| 189 | Goodenough_Samuel | Smith_Sir_James_Edward | 222 |
| 413 | Smith_Sir_James_Edward | Macleay_Alexander | 102 |
| 525 | Woodward_Thomas_Jenkinson | Smith_Sir_James_Edward | 101 |
| 348 | Roscoe_William | Smith_Sir_James_Edward | 94 |
| 241 | Johnes_Thomas | Smith_Sir_James_Edward | 83 |
| ... | ... | ... | ... |
| 352 | Rous_Charlotte_Maria | Smith_Sir_James_Edward | 1 |
| 353 | Rowden_Frances_Arabella | Smith_Sir_James_Edward | 1 |
| 158 | Erskine_David_Steuart | Smith_Sir_James_Edward | 1 |
| 157 | Engelhart_John_Henry | Smith_Sir_James_Edward | 1 |
| 533 | Zimmermann_Eberhard_August_Wilhelm | Smith_Sir_James_Edward | 1 |
534 rows × 3 columns
Z těchto dat si nyní vytvoříme síťový objekt.
G = nx.from_pandas_edgelist(linnean_edges, 'sender_agr', 'recipient_agr', 'letters_n', create_using=nx.DiGraph())
type(G)
networkx.classes.digraph.DiGraph
Základní vlastnosti, které nás o našem grafu zajímají jsou, kolik má uzlů a kolik má hran?
G.number_of_nodes()
476
G.number_of_edges()
534
Další užitečnou informací je, kolik mají uzle v průměru vazeb (tzv. avarege degree).
sum(dict(G.degree).values()) / G.number_of_nodes()
2.2436974789915967
Stejně tak zajímavé bude se podívat, které uzly mají nejvyšší in-degree (tj. vazeb do něj vstupujících) a out-degree (tj. vazeb z něj vystupujících). Podívejme se na deset uzlů s nejvyšší hodnotou in-degree:
sorted(dict(G.in_degree()).items(), key=lambda item: item[1], reverse=True)[:10]
[('Smith_Sir_James_Edward', 445),
('Unknown_nan', 14),
('Smith_Pleasance', 6),
('Cullum_Sir_Thomas_Gery', 4),
('Lambert_Aylmer_Bourke', 2),
('Wallich_Nathaniel', 2),
('Goodenough_Samuel', 2),
('The_Linnean_Society_nan', 2),
('Banks_Sir_Joseph', 1),
('Barrington_Shute', 1)]
Vidíme, že zcela ústřední pozici zde zaujímá Sir James Edward Smith, zakladatel a dlouholetý předseda společnosti. Hned na druhém místě se v jednom uzlu potkávají dopisy, jejichž adresát je neznámý. Nebude od věci tento uzel ze sítě zcela odstranit.
G.remove_node("Unknown_nan")
Utvořený síťový graf si můžeme bezprostřdně vizualizovat pomocí funkce nx.draw():
nx.draw(G)
Bohužel vidíme, že výsledek vypadá spíše nevábně. Podle všeho se zde příliš mnoho uzlů poblíž středu. Vidíme, že vazby mají podobu šipek. Je tomu tak proto, že se jedná o tzv. směrový graf.
Abychom dosáhli lepších výsledků, přidáme do vizualizační funkce několik dodatečných parametrů
my_color = "darkgreen" # vybereme jakoukoli jinou barvu odtud: https://matplotlib.org/stable/gallery/color/named_colors.html
nx.draw(G, node_size=20, node_color=my_color, pos=nx.kamada_kawai_layout(G))
# Tato buňka slouží ke kontrole průchodu tímto cvičením.
# Pokud toto cvičení plníte v rámci svých studijních povinností na ZČU, buňku spusťte a držte se instrukcí.
import requests
exec(requests.get("https://sciencedata.dk/shared/856b0a7402aa7c7258186a8bdb329bd3?download").text)
kontrola_pruchodu(ntb="site", arg1=my_color)
Uzly v grafu se jmenují stejně jako korespondenti. Pomocí syntaxe níže se tak můžeme podívat na vlastnosti jednotlivých vazeb.
G["Smith_Sir_James_Edward"]["Macleay_Alexander"]
{'letters_n': 102}
G["Macleay_Alexander"]["Smith_Sir_James_Edward"]
{'letters_n': 74}
Zde se dozvídáme, že zatímco Sir James Edward Smith poslal Alexanderu Macleayovi 102, v opačném směru jich šlo 74.
Pro některé typy analýz je praktičtější i smysluplnější pracovat s nesměrovým grafem. Vazba tak nezohledňuje směr příslušné korespondence a váha může odpovídat součtu vyměněných dopisů v obou směrech. Transformovat naši síť do této podoby vyžaduje několik řádek kódu, jimiž se zde nemusíme příliš zaobírat, důležitější je výsledek.
to_remove = []
edges_met = []
for node1, node2 in G.edges():
if (G.has_edge(node2, node1)) & ((node2, node1) not in edges_met):
G[node1][node2]["letters_n"] = G[node1][node2]["letters_n"] + G[node2][node1]["letters_n"]
to_remove.append((node2, node1))
edges_met.append((node1, node2))
for u,v in to_remove:
G.remove_edge(u,v)
G = G.to_undirected().copy()
len(G.edges())
484
Zde nyní uvidíme, že v obou směrech je hodnota "letters_n" totožná:
G["Smith_Sir_James_Edward"]["Macleay_Alexander"]
{'letters_n': 176}
G["Macleay_Alexander"]["Smith_Sir_James_Edward"]
{'letters_n': 176}
weighted_degrees = {}
for node in G.nodes():
weighted_degrees[node] = G.degree(node, weight='letters_n')
list(weighted_degrees.items())[:10]
[('Abbot_Charles', 18),
('Smith_Sir_James_Edward', 3418),
('Acharius_Erik', 8),
('Acrel_Johan_Gustaf', 7),
('Afzelius_Adam', 14),
('Aiton_William_Townsend', 1),
('Allioni_Carlo', 7),
('Anderson_Alexander', 2),
('Anderson_James', 2),
('Anguish_Mrs_S', 1)]
# tento degree učiníme atributem našich uzlů
nx.set_node_attributes(G, weighted_degrees, 'weighted_degree')
Nyní si vyjmeme pouze uzly, které mají stupeň (degree) alespoň roven 2, tj. uzly osob, kteří v našem datasetu vedly korespondenci s více než jednou osobou.
node_list = [node for node in G.nodes if G.degree(node) >= 2]
len(node_list)
28
Ukazuje se, že takových uzlů je v našem datasetu relativně málo. Vypišme si jejich jména.
node_list
['Smith_Sir_James_Edward', 'Barrington_Jane', 'Lambert_Aylmer_Bourke', 'Sutton_Charles', 'Bicheno_James_Ebenezer', 'Forster_Edward', 'Boyd_George', 'Roxburgh_William', 'Brodie_James', 'Coke_Thomas_William', 'Wallich_Nathaniel', 'Crowe_James', 'Cullum_Sir_Thomas_Gery', 'Smith_Pleasance', 'Davy_Martin', 'Don_George', 'Goodenough_Samuel', 'Drake_William_Fitt', 'Gemmellaro_Carlo', 'The_Linnean_Society_nan', 'Gurney_Anna', 'Harriman_John', 'Johnes_Thomas', 'Latham_John', 'Martyn_Thomas', 'Smith_James', 'Swartz_Olof_Peter', 'Webb_William']
Nyní tento seznam jmen využijeme k vymezení výseku z našeho grafu (nazveme si jej Gsub), který bude zahrnovat pouze tyto uzly.
Gsub = G.subgraph(node_list)
fig, ax = plt.subplots(1,1, figsize=(9, 6), dpi=300, tight_layout=True)
# pro potřeby vizualizace si ještě definujeme šířku čar jednotlivých vazeb,vycházející z objemu vyměněných dopisů.
edge_widths = [np.sqrt(d['letters_n']) / 2 for (u, v, d) in Gsub.edges(data=True)]
nx.draw(Gsub, with_labels=True, pos=nx.kamada_kawai_layout(Gsub), node_size=100, nodelist=node_list, width=edge_widths, ax=ax)
ax.set_xlim(-1.3, 1.3)
(-1.3, 1.3)
Z takovéto vizualizace již lze vypozorovat leccos.
Cvičení 2: Britská vědecká korespondence dlouhého 19. století jako celek¶
Extrace a předzpracování dat¶
Nyní se vrátíme na začátek. Projekt Ɛpsilon totiž hostí vícero kolekcí dopisů z podobného období a je na místě očekávat, že se osoby v těchto kolekcích budou alespoň částečně překrývat.
Vypišme si tedy nejprve jména csv souborů s metadaty k těmto kolekcím.
resp_json = requests.get("https://api.github.com/repos/cambridge-collection/epsilon-data/contents/csv/").json()
download_urls = [item["download_url"] for item in resp_json]
download_urls
['https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/ampere.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/darwin-correspondence.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/darwin-family-letters.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/faraday.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/henslow.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/herschel.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/kemp.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/linnean-society.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/royal-society.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/somerville.csv', 'https://raw.githubusercontent.com/cambridge-collection/epsilon-data/main/csv/tyndall.csv']
Nyní pomocí cyklu FOR načteme data ze všech těchto souborů a nakonec je spojíme do jednoho objektu type pd.DataFrame.
dfs = [] # připrav prázdný seznam, který budeme následně postupně plnit daty z jednotlivých kolekcí
for url in download_urls: # pro každý z našeho seznamu souborů:
try: # zkus: jej načíst jako dataframe
collection_df = pd.read_csv(url, on_bad_lines='skip')
collection_df["source"] = url.rpartition("/")[2] # přidej tomuto dataframu nový sloupec "source", kde bude uvedeno jméno souboru, ze kterého pochází
dfs.append(collection_df) # přidej do seznamu aktuální dataframe
except: # pokud to nejde:
print("failed: ", url) # vypiš jméno souboru, u kterého to nejde
epsilon = pd.concat(dfs) # spoj do jednoho všechny dataframy uvnitř seznamu dfs
epsilon.head(5)
| id | sender_surname | sender_forename | recipient_surname | recipient_forename | sorting_date | date | sender_address | recipient_address | source | languages | extent | filename | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | L1 | Ampère | Jeanne-Antoinette (mère d'Ampère) | Ampère | André-Marie | 1775-01-01 | s.d. | NaN | NaN | ampere.csv | fra | NaN | L1.xml |
| 1 | L2 | Maine de Biran | Pierre | Ampère | André-Marie | 1807-03-15 | 15 mars 1807 | NaN | NaN | ampere.csv | fra | NaN | L2.xml |
| 2 | L3 | Ampère | André-Marie | Ampère | Jean-Jacques (fils d'Ampère) | 1775-01-01 | s.d. | NaN | NaN | ampere.csv | fra | NaN | L3.xml |
| 3 | L4 | Ampère | André-Marie | Duhamel | Jean-Marie | 1775-01-01 | s.d. | NaN | NaN | ampere.csv | fra | NaN | L4.xml |
| 4 | L5 | Ampère | André-Marie | Duhamel | Jean-Marie | 1775-01-01 | s.d. | NaN | NaN | ampere.csv | fra | NaN | L5.xml |
# jak dlouhý je náš dataset?
len(epsilon)
48995
# stejně jako výše agregujme jména autorů a příjemců dopisů do podoby bez mezer a závorek
epsilon["sender_agr"] = epsilon.apply( lambda row: str(row["sender_surname"]).replace(" ", "_").partition(" (")[0] + "_" + str(row["sender_forename"]).replace(" ", "_").partition(" (")[0], axis=1)
epsilon["sender_agr"] = epsilon["sender_agr"].apply(lambda x: regex.sub("[^\p{L}_-]", "", x))
epsilon["recipient_agr"] = epsilon.apply( lambda row: str(row["recipient_surname"]).replace(" ", "_").partition(" (")[0] + "_" + str(row["recipient_forename"]).replace(" ", "_").partition(" (")[0], axis=1)
epsilon["recipient_agr"] = epsilon["recipient_agr"].apply(lambda x: regex.sub("[^\p{L}_-]", "", x))
# odstraníme neznáme odesilatele a příjemce
epsilon = epsilon[~epsilon.isin(["Unknown_nan", "AT_TO_LOOK", "nan_nan"]).any(axis=1)]
<>:3: SyntaxWarning: invalid escape sequence '\p'
<>:6: SyntaxWarning: invalid escape sequence '\p'
<>:3: SyntaxWarning: invalid escape sequence '\p'
<>:6: SyntaxWarning: invalid escape sequence '\p'
/var/folders/57/tg7c_g894t5c2z3swkqzds5h0000gn/T/ipykernel_3127/1866689768.py:3: SyntaxWarning: invalid escape sequence '\p'
epsilon["sender_agr"] = epsilon["sender_agr"].apply(lambda x: regex.sub("[^\p{L}_-]", "", x))
/var/folders/57/tg7c_g894t5c2z3swkqzds5h0000gn/T/ipykernel_3127/1866689768.py:6: SyntaxWarning: invalid escape sequence '\p'
epsilon["recipient_agr"] = epsilon["recipient_agr"].apply(lambda x: regex.sub("[^\p{L}_-]", "", x))
Díky attributu "source" se vždy můžeme podívat pouze na výsek dat z konkrétního zdroje:
epsilon[epsilon["source"]=="darwin-family-letters.csv"].head(5)
| id | sender_surname | sender_forename | recipient_surname | recipient_forename | sorting_date | date | sender_address | recipient_address | source | languages | extent | filename | sender_agr | recipient_agr | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | FL-0001 | Darwin | G. H. | Darwin | Emma | 1868-03-01 | [late March 1868] | NaN | NaN | darwin-family-letters.csv | NaN | NaN | FL-0001.xml | Darwin_G_H | Darwin_Emma |
| 1 | FL-0002 | Darwin | G. H. | Darwin | H. E. | 1869-04-13 | [probably 13 April 1869] | 84. Chps. Elysées | NaN | darwin-family-letters.csv | NaN | NaN | FL-0002.xml | Darwin_G_H | Darwin_H_E |
| 2 | FL-0003 | Darwin | G. H. | Darwin | H. E. | 1873-01-25 | 25 January 1873 | Hotel de Provence | Cannes | NaN | darwin-family-letters.csv | NaN | NaN | FL-0003.xml | Darwin_G_H | Darwin_H_E |
| 3 | FL-0004 | Darwin | G. H. | Darwin | Emma | 1873-02-27 | 27 February [1873] | Hotel de Provence, Cannes | NaN | darwin-family-letters.csv | NaN | NaN | FL-0004.xml | Darwin_G_H | Darwin_Emma |
| 4 | FL-0005 | Darwin | G. H. | Darwin | Emma | 1873-03-03 | 3 March 1873 | Hotel de Provence | Cannes | NaN | darwin-family-letters.csv | NaN | NaN | FL-0005.xml | Darwin_G_H | Darwin_Emma |
Vypišme si nejplodnější autory a nejpopulárnější příjemce:
epsilon["sender_agr"].value_counts()
sender_agr
Darwin_C_R 8151
Herschel_Sir_John 5353
Faraday_Michael 2984
Tyndall_John 1146
Darwin_Emma 884
...
Fry_C_E 1
Radovanović_Marinko 1
Adams_A_L 1
Ledeganck_Kasimir 1
Bohn_Johann_C 1
Name: count, Length: 5772, dtype: int64
epsilon["recipient_agr"].value_counts()
recipient_agr
Herschel_Sir_John 9305
Darwin_C_R 6713
Smith_Sir_James_Edward 2946
Faraday_Michael 2102
Tyndall_John 1291
...
Avogadro_Amedeo 1
Herbert_Faraday_Jacob 1
Ehrenberg_Christian_Gottfried 1
Sievier_Robert_William 1
Clausius_Adelheid 1
Name: count, Length: 3895, dtype: int64
Tentokrát si data vazeb do nesměrové podoby převedeme ještě před vytvořením grafu.
epsilon_temp = epsilon.apply(lambda row: pd.Series(sorted([str(row["sender_agr"]), str(row["recipient_agr"])])), axis=1)
epsilon_temp.columns = ["node1", "node2"]
epsilon_edges = epsilon_temp.groupby(["node1", "node2"]).size().reset_index()
epsilon_edges.columns = ["node1", "node2", "weight"]
epsilon_edges = epsilon_edges[epsilon_edges["node1"] != epsilon_edges["node2"]]
epsilon_edges.head(5)
| node1 | node2 | weight | |
|---|---|---|---|
| 0 | AB_Hewetson_nan | Tristram_Henry_Baker | 1 |
| 1 | AB_nan | Faraday_Michael | 2 |
| 2 | AW_Williamson_Foreign_Secretary_Royal_Society | Williamson_Alexander_William | 1 |
| 3 | A_B | Royal_Society_nan | 1 |
| 4 | A_H_White_Royal_Society | R_L_Sheppard_Tropical_Diseases_Bureau | 1 |
Data v této podobě můžeme již neprodleně použít k tvorbě sítě váženého nesměrového grafu.
G = nx.from_pandas_edgelist(epsilon_edges, 'node1', 'node2', 'weight')
Opět se nejprve podíváme, z kolika uzlů a kolika hran naše síť sestává:
G.number_of_nodes()
7608
G.number_of_edges()
9018
Z těchto dat lze také snadno vypočítat tzv. average degree:
(2 * G.number_of_edges()) / G.number_of_nodes()
2.3706624605678233
U grafu s takto velkým počtem uzlů se nezřídka stane, že se ukáže, že je ve skutečnosti tvořen několika oddělenými komponenty, čili že síť není zcela propojená.
len(list(nx.connected_components(G)))
175
Ano, to je i náš případ zde, když máme co dočinění s grafem, který sestává z více než 160 komponentů.
Podívejme se, z kolika uzlů sestává deset největších komponentů:
components_sorted = sorted(list(nx.connected_components(G)), key=len, reverse=True)
[len(comp) for comp in components_sorted][:10]
[7235, 5, 4, 4, 4, 3, 3, 3, 3, 3]
Vidíme, že většina uzlů je součástí největšího komponentu, druhý největší komponent sestává již pouze z 5 uzlů. S klidným svědomím se nyní zaměříme pouze na největší komponent naší sítě.
len(components_sorted[0])
7235
# Omezíme se na největší komponent.
G = G.subgraph(list(components_sorted[0]))
G.number_of_nodes() #zkontrolujeme, že se filtrace uzlů povedla
7235
(2 * G.number_of_edges()) / G.number_of_nodes()
2.437595024187975
Pro potřeby několika dalších vizualizací nyní všem uzlům v rámci této sítě přiřadíme pozici v prostoru na základě jejich strukturelního postavení. Přiřazení těchto pozic v případě sítě, která sestává z tisíců uzlů, může být výpočetně poměrně náročné a zabrat nějaký čas. Abychom se níže vyhnuli zbytečnému čekání, vypočteme si tyto pozice uzlů již zde a dále je budeme používat v několika vizualizacích po sobě.
%%time
pos = nx.spring_layout(G)
CPU times: user 50.1 s, sys: 103 ms, total: 50.2 s Wall time: 50.5 s
fig, ax = plt.subplots(figsize=(9,6), dpi=300)
nx.draw(G, node_size=10, node_color="darkgreen", pos=pos, ax=ax)
# Tato buňka slouží ke kontrole průchodu tímto cvičením.
# Pokud toto cvičení plníte v rámci svých studijních povinností na ZČU, buňku spusťte a držte se instrukcí.
kontrola_pruchodu(ntb="site", arg1="site2")
Zde končí povinná část cvičení.
Tato síť již možná má některé zajímavé topografické vlastnosti, které si zaslouží bližší analytické ohledání.
Metriky centrality¶
Jedna skupina populárních a užitečných algoritmů jsou tzv. metriky centrality uzlů či vazeb. Uveďme si dvě takové metriky s jejich anglickými názvy a krátkým vysvětlením nejznámnější s jejich anglickými názvy:
- degree centrality: je definován počtem vazeb, které daný uzel má
- closeness centrality: součet vzdáleností nejkratších cest potřebných k dosažení všech ostatních uzlů uvnitř sítě.
- betweenness centrality (mezilehlost): Jak často se ten který uzel nachází na trase spojující nejkratší cestou jakékoli další uzly uvnitř sítě.
- PageRank centrality: je určen mnohonásobně opakovanými náhodnými procházkami po síti. Velikost PageRank je určena množstvím návštěv daného uzlu při těchto procházkách. Tento algoritmus byl původně vyvinut vývojáři od společnosti Google pro určení důležitých webových stránek.
S degree centrality jsme již vlastně pracovali, když jsme se u předchozí sítě omezili pouze na uzly s degree alespoň 2. Tato metrika je také nejsnáze srozumitelná a bude zajímavé si zde představit její výsledky pro potřeby srovnání s výsledky ostatních metrik. Jelikož zde však pracujeme s relativně rozsáhlou sítí a náš společný čas je omezený, vyzkoušíme si nyní pouze algrotimus pro PageRank, který je výpočetně nejméně náročný.
degree_centrality = nx.degree_centrality(G)
degree_top_nodes = sorted(degree_centrality.items(), key=lambda x:x[1], reverse=True)
degree_top_nodes[:10]
[('Darwin_C_R', 0.2756427978988112),
('Herschel_Sir_John', 0.2456455626209566),
('Faraday_Michael', 0.16284213436549627),
('Smith_Sir_James_Edward', 0.06331213713021841),
('Tyndall_John', 0.05322090129941941),
('Henslow_J_S', 0.041747304395908215),
('Royal_Society_nan', 0.029167818634227263),
('Ampère_André-Marie', 0.026264860381531658),
('Banks_Joseph', 0.019491291125241915),
('Somerville_Mary', 0.01631186065800387)]
pagerank_centrality = nx.pagerank(G, max_iter=10000)
pagerank_top_nodes = sorted(pagerank_centrality.items(), key=lambda x:x[1], reverse=True)
pagerank_top_nodes[:10]
[('Darwin_C_R', 0.12278734164130309),
('Herschel_Sir_John', 0.11635094260032468),
('Faraday_Michael', 0.05784230368853712),
('Smith_Sir_James_Edward', 0.02876745325639608),
('Tyndall_John', 0.02263122211784177),
('Henslow_J_S', 0.013202863393256076),
('Ampère_André-Marie', 0.01204361057055285),
('Hooker_J_D', 0.010207314044604829),
('Airy_George_Biddell', 0.009547965412064574),
('Royal_Society_nan', 0.008285770649660521)]
%%time
betweenness_centrality = nx.betweenness_centrality(G)
betweenness_top_nodes = sorted(betweenness_centrality.items(), key=lambda x:x[1], reverse=True)
betweenness_top_nodes[:10]
CPU times: user 2min 40s, sys: 377 ms, total: 2min 40s Wall time: 2min 40s
[('Darwin_C_R', 0.469108500528852),
('Herschel_Sir_John', 0.4065187536309389),
('Faraday_Michael', 0.3068789118436766),
('Smith_Sir_James_Edward', 0.11431433729309902),
('Royal_Society_nan', 0.1036044758898639),
('Tyndall_John', 0.09594590876852578),
('Henslow_J_S', 0.06581531584104831),
('Banks_Joseph', 0.05431315049556383),
('Ampère_André-Marie', 0.049047409810552424),
('Watson_William', 0.0340065780538162)]
degree_pagerank_comparison = []
for deg, page, betw in zip(degree_top_nodes, pagerank_top_nodes, betweenness_top_nodes):
degree_pagerank_comparison.append([deg[0], page[0], betw[0]])
centr_comparison_df = pd.DataFrame(degree_pagerank_comparison)
centr_comparison_df.columns = ["degree_node", "pagerank_node", "betw_node"]
print(centr_comparison_df.head(20).round(2))
degree_node pagerank_node betw_node 0 Darwin_C_R Darwin_C_R Darwin_C_R 1 Herschel_Sir_John Herschel_Sir_John Herschel_Sir_John 2 Faraday_Michael Faraday_Michael Faraday_Michael 3 Smith_Sir_James_Edward Smith_Sir_James_Edward Smith_Sir_James_Edward 4 Tyndall_John Tyndall_John Royal_Society_nan 5 Henslow_J_S Henslow_J_S Tyndall_John 6 Royal_Society_nan Ampère_André-Marie Henslow_J_S 7 Ampère_André-Marie Hooker_J_D Banks_Joseph 8 Banks_Joseph Airy_George_Biddell Ampère_André-Marie 9 Somerville_Mary Royal_Society_nan Watson_William 10 Folkes_Martin Banks_Joseph Folkes_Martin 11 Mortimer_Cromwell Sabine_Edward Somerville_Mary 12 Birch_Thomas Darwin_Emma Lyell_Charles 13 Wedgwood_Emma Somerville_Mary Mortimer_Cromwell 14 Darwin_G_H Darwin_W_E Maskelyne_Nevil 15 Herschel_Margaret_Brodie Darwin_G_H Cooper_William 16 Pringle_John Babbage_Charles Wright_Thomas 17 Darwin_Francis Mortimer_Cromwell Sabine_Edward 18 Maskelyne_Nevil Folkes_Martin Birch_Thomas 19 Sabine_Edward Herschel_Margaret_Brodie Pringle_John
V čem je toto srovnání potenciálně zajímavé? Podíváme-li se na pravou stranu tabulky, tj. uzly s největší betweenness centralitou, vidíme, že zejména ve druhé desítce se nachází nemálo uzlů, se kterými se na levé straně (u degree centrality) v první dvacítce vůbec nesetkáváme: Jinými slovy, jedná se o uzly, jejichž centralita v rámci sítě není živena výlučně množstvím vazeb, které uvnitř sítě mají, ale spíše specifickým strukturálním postavením. Podívejme se tedy na stejná data ještě jiným způsobem a totiž vypišme si, na kolikáté pozici se dvacítka uzlů s nejvyšší beteweenness centrality nachází z hlediska degree centrality.
for node in centr_comparison_df["betw_node"][:20]:
print(node, " degree:", G.degree(node), "degree rank:", [el[0] + 1 for el in enumerate(degree_top_nodes) if el[1][0] == node][0], )
Darwin_C_R degree: 1994 degree rank: 1 Herschel_Sir_John degree: 1777 degree rank: 2 Faraday_Michael degree: 1178 degree rank: 3 Smith_Sir_James_Edward degree: 458 degree rank: 4 Royal_Society_nan degree: 211 degree rank: 7 Tyndall_John degree: 385 degree rank: 5 Henslow_J_S degree: 302 degree rank: 6 Banks_Joseph degree: 141 degree rank: 9 Ampère_André-Marie degree: 190 degree rank: 8 Watson_William degree: 38 degree rank: 23 Folkes_Martin degree: 71 degree rank: 11 Somerville_Mary degree: 118 degree rank: 10 Lyell_Charles degree: 20 degree rank: 41 Mortimer_Cromwell degree: 67 degree rank: 12 Maskelyne_Nevil degree: 40 degree rank: 19 Cooper_William degree: 3 degree rank: 312 Wright_Thomas degree: 5 degree rank: 195 Sabine_Edward degree: 40 degree rank: 20 Birch_Thomas degree: 65 degree rank: 13 Pringle_John degree: 44 degree rank: 17
Podívejme se nyní čtyři osobnosti:
- Charles Lyell
- Francis Galton
- John Phillips
- John Lubbock
Jejich degree rank je ve srovnání s jejich betweenness relativně vysoký. Zdá se, že tedy uzly mají v rámci grafu strukturálně zajímovou pozici.
Vytvořme tedy novou vizualizaci, v rámci které zaostříme pozornost právě na 20 uzlů s největší betweenness. Tyto uzly vyobrazíme odlišnou barvou a stejnou barvou vyobrazíme i jejich jména.
special_nodes = centr_comparison_df["betw_node"][:20] #["Lyell_Charles", "Galton_Francis", "Phillips_John", "Lubbock_John"]
special_pos = dict([(node, pos[node]) for node in special_nodes])
labels = {node: node for node in special_nodes}
fig, ax = plt.subplots(figsize=(24,18), dpi=300)
special_nodes_color = "darkorange"
nx.draw(G, node_size=10, node_color="black", edge_color="grey",pos=pos, ax=ax, alpha=0.5)
nx.draw_networkx_nodes(G, nodelist=special_nodes, node_size=50, node_color=special_nodes_color, pos=special_pos, ax=ax)
nx.draw_networkx_labels(G, font_color=special_nodes_color, pos=special_pos, labels=labels,ax=ax)
{'Darwin_C_R': Text(0.25515463948249817, 0.09449182450771332, 'Darwin_C_R'),
'Herschel_Sir_John': Text(-0.07416675239801407, 0.11250030249357224, 'Herschel_Sir_John'),
'Faraday_Michael': Text(-0.004509699996560812, -0.05904305726289749, 'Faraday_Michael'),
'Smith_Sir_James_Edward': Text(-0.2566421926021576, -0.2449042797088623, 'Smith_Sir_James_Edward'),
'Royal_Society_nan': Text(-0.4172230362892151, -0.24310152232646942, 'Royal_Society_nan'),
'Tyndall_John': Text(0.026099059730768204, 0.13985076546669006, 'Tyndall_John'),
'Henslow_J_S': Text(0.09531351923942566, 0.011145846918225288, 'Henslow_J_S'),
'Banks_Joseph': Text(-0.04085996374487877, 0.10708943754434586, 'Banks_Joseph'),
'Ampère_André-Marie': Text(0.13156390190124512, -0.26625972986221313, 'Ampère_André-Marie'),
'Watson_William': Text(-0.3181363642215729, -0.3269495964050293, 'Watson_William'),
'Folkes_Martin': Text(-0.465585857629776, -0.4521389603614807, 'Folkes_Martin'),
'Somerville_Mary': Text(-0.10781338065862656, 0.06948250532150269, 'Somerville_Mary'),
'Lyell_Charles': Text(0.14846399426460266, 0.05960281565785408, 'Lyell_Charles'),
'Mortimer_Cromwell': Text(-0.5961413979530334, -0.5324532389640808, 'Mortimer_Cromwell'),
'Maskelyne_Nevil': Text(-0.20375408232212067, 0.03627181425690651, 'Maskelyne_Nevil'),
'Cooper_William': Text(-0.09378960728645325, -0.03636864572763443, 'Cooper_William'),
'Wright_Thomas': Text(-0.136579230427742, -0.13542836904525757, 'Wright_Thomas'),
'Sabine_Edward': Text(-0.028930403292179108, 0.0886259526014328, 'Sabine_Edward'),
'Birch_Thomas': Text(-0.5180448293685913, -0.329683393239975, 'Birch_Thomas'),
'Pringle_John': Text(-0.23939625918865204, -0.39142242074012756, 'Pringle_John')}
Aby byl text čitelný a graf přehledný, vizualizace výše je výrazně větší než ty předchozí. Uložíme si ji do samostatného souboru ve formátu png.
try:
fig.savefig("../figures/epsilon_betw.png") # pokud pracujeme s repozitoří jako celkem, včetně podlsožky "figures"
except:
fig.savefig("epsilon_betw.png") # pokud pracujeme s notebookem samostatně, např. přes Google Colab
Detekce komunit¶
Další důležitou rodinou algoritmů jsou algoritmy pro detekování komunit, neboli shluků uzlů, které jsou mezi sebou provázány více, než z uzly z jejich okolí. Zde použijeme takzvanou Lovaňskou metodu (podle působiště výzkumníků, kteří ji vyvinuli [viz wikipedia]). Tento algoritmus se snaží nalézt takové rozdělení uzlů do komunit, které maximalizuje poměr vazeb mezi uzly uvnitř těchto komunit oproti jejich vazbám směrem ven z těchto komunit.
from networkx.algorithms import community
communities = nx.community.louvain_communities(G, seed=1)
len(communities)
18
Algoritmus identifikoval 16 komunit. Podívejme se nejprve, kolik jednotlivé komunity čítají uzlů:
[len(com) for com in communities]
[1109, 1686, 1959, 184, 939, 260, 102, 93, 2, 3, 332, 2, 3, 6, 2, 456, 9, 88]
cmap = plt.get_cmap('viridis')
colors = [cmap(i) for i in np.linspace(0, 1, len(communities))]
fig, ax = plt.subplots(figsize=(24,18), dpi=300)
nx.draw_networkx_edges(G, edge_color="grey",pos=pos, alpha=0.5, ax=ax)
for community, color in zip(communities, colors):
special_pos = dict([(node, pos[node]) for node in list(community)])
#nx.draw(G, node_size=10, node_color="black", edge_color="grey",pos=pos, ax=ax, alpha=0.5)
nx.draw_networkx_nodes(G, nodelist=list(community), node_size=10, node_color=[color], pos=special_pos, ax=ax)
ax.axis('off')
(-1.1997491666674613, 1.102122182548046, -1.0986506187915803, 1.1092220985889434)
Vidíme, že tento algoritmus tedy dokáže velice pěkně zachytit strukturální vlastnosti dané sítě. To je v případě rozsáhlých grafů velice užitečné.
correspsearch = pd.read_csv("https://correspsearch.net/api/v2.0/csv.xql?", sep=";")
correspsearch.head(10)
%%time
for n in range(2,30):
page_df = pd.read_csv("https://correspsearch.net/api/v2.0/csv.xql?x=" + str(n), sep=";")
correspsearch = pd.concat([correspsearch, page_df])
if n in range(0,3000,100):
print(n)
if len(page_df) < 100:
break
len(correspsearch)
correspsearch = correspsearch[correspsearch["sender"].notnull() & correspsearch["addressee"].notnull()]
len(correspsearch)
